﻿using log4net;
using MudEngine.Parsing.AST;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace MudEngine.Parsing
{
	public abstract class Identifier
	{
		/// <summary>
		/// 
		/// </summary>
		public int Address { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public int Index { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public AType Type { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public Scope Scope { get; set; }
    }

	public class Function : Identifier
	{
        public const string INIT_FUNC = "___INIT";
		/// <summary>
		/// 
		/// </summary>
		private Variable[] _locals;
		/// <summary>
		/// 
		/// </summary>
		private int _max_local = -1;
        /// <summary>
		/// 
		/// </summary>
		public int NumArguments { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public int NumLocals { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public FunctionFlags Flags { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int EndAddress { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public Variable[] Locals
		{
			get
			{
				if (_locals == null)
					_locals = new Variable[this.NumArguments + NumLocals];
				return _locals;
			}
		}
		/// <summary>
		/// Returns the first local variable with the given name
		/// </summary>
		/// <param name="name"></param>
		/// <returns></returns>
		public Variable FindVariable(string name)
		{
			for (int i = 0; i < this.Locals.Length; i++)
			{
				if (this.Locals[i] != null && this.Locals[i].Name.Equals(name))
					return this.Locals[i];
			}
			return null;
		}
		/// <summary>
		/// Adds a new variable to the list of local variables
		/// </summary>
		/// <param name="name"></param>
		/// <param name="type"></param>
		public Variable AddVariable(string name, AType type)
		{
			if (this._max_local == -1)
				this._max_local = this.NumArguments;

			Variable var = new Variable(name, type, this._max_local++, Scope.Local);
			this.Locals[var.Index] = var;

			return var;
		}
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0} {1}()", this.Type, this.Name);
        }
    }

    public delegate void VoidPointer();

    public class DelegatedFunction : Function
    {
        public VoidPointer Function { get; set; }
    }

	public enum Scope
	{
		Global,
		Local,
		Parameter
	}

    public enum FunctionFlags
    {
        Prototype = 1,
        Final = 2,
        Runtime = 4,
        VarArgs = 8,
        Inherited = 16
    }

	public class Variable : Identifier
	{
		/// <summary>
		/// 
		/// </summary>
		/// <param name="name"></param>
		/// <param name="resultType"></param>
		/// <param name="idx"></param>
		public Variable(string name, AType resultType, int idx, Scope varType)
		{
			this.Name = name;
			this.Type = resultType;
            this.Address = this.Index = idx;
			this.Scope = varType;
		}
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0} {1}", this.Type, this.Name);
        }
    }

    public class Inherit
    {
        public int Index { get; set; }

        public string Filename { get; set; }

        public Program Program { get; set; }

        public Function Initalizer { get; set; }

        public int VariableIndexOffset { get; set; }

        public int FunctionIndexOffset { get; set; }
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("\"{0}\" - loaded: {1}", this.Filename, (this.Program == null?"no":"yes"));
        }
    }

    public class Breakpoint
    {
        public int Address { get; private set; }

        public OpCodes OrigOpCode { get; private set; }

        public int RefCount { get; set; }

        public Breakpoint(int address, OpCodes origOpCode)
        {
            this.Address = address;
            this.OrigOpCode = origOpCode;
            this.RefCount = 1;
        }
    }

    public class InheritedEntry
    {
        public Function Function { get; set; }

        public Program Program { get; set; }
    }

	public class Program
	{
        private static ILog _log = Log.GetLogger(typeof(Program));
        /// <summary>
        /// 
        /// </summary>
        public static Version _STRUCT_VER = Assembly.GetExecutingAssembly().GetName().Version;
		/// <summary>
		/// 
		/// </summary>
		public string Filename { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public DateTime LastChanged { get; set; }
		/// <summary>
		/// 
		/// </summary>
        public byte[] Binary { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public int ProgramSize { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public StringCollection StringTable { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public List<Function> FunctionTable { get; set; }
		/// <summary>
		/// 
		/// </summary>
		public List<Variable> Variables { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public List<Inherit> Inherits { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int NumberOfVariables { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public int TotalNumberOfVariables { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public Dictionary<int, Breakpoint> Breakpoints { get; private set; }
        /// <summary>
        /// 
        /// </summary>
        public Program()
        {
            this.Breakpoints = new Dictionary<int, Breakpoint>(1);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="address"></param>
        /// <returns></returns>
        public bool SetBreakpoint(int address)
        {
            OpCodes origOpCode = this.IsInstruction(address);
            if ((byte)origOpCode < byte.MaxValue)
            {
                if (this.Breakpoints.ContainsKey(address))
                    this.Breakpoints[address].RefCount++;
                else
                {
                    Breakpoint bp = new Breakpoint(address, origOpCode);
                    this.Breakpoints.Add(address, bp);
                    this.Binary[address] = (byte)OpCodes.bp;
                }
                return true;
            }
            return false;
        }
		/// <summary>
		/// Read the OpCode at the given program position
		/// </summary>
		/// <returns></returns>
		public OpCodes ReadOpCode(int pc)
		{
			return (OpCodes)this.Binary[pc];
		}
		/// <summary>
		/// Reads 1 byte from the current program
		/// </summary>
		/// <returns></returns>
		public byte Load1(int pc)
		{
			return this.Binary[pc];
		}
		/// <summary>
		/// Reads 2 bytes from the current program
		/// </summary>
		/// <returns></returns>
		public short Load2(int pc)
		{
			int res = (this.Binary[pc++] << 8) + this.Binary[pc];
			return (short)res;
		}
		/// <summary>
		/// Reads 4 bytes from the current program
		/// </summary>
		/// <returns></returns>
		public int Load4(int pc)
		{
			return (this.Binary[pc++] << 24) + (this.Binary[pc++] << 16) + (this.Binary[pc++] << 8) + this.Binary[pc++];
		}
		/// <summary>
		/// Reads 4 bytes from the current program
		/// </summary>
		/// <returns></returns>
		public long Load8(int pc)
		{
			long result = this.Binary[pc++] << 56;
			result += this.Binary[pc++] << 48;
			result += this.Binary[pc++] << 40;
			result += this.Binary[pc++] << 32;
			result += this.Binary[pc++] << 24;
			result += this.Binary[pc++] << 16;
			result += this.Binary[pc++] << 8;
			result += this.Binary[pc++];
			return result;
		}
		/// <summary>
		/// 
		/// </summary>
		/// <param name="functionName"></param>
		/// <returns></returns>
		internal Function FindFunction(string functionName)
		{
			return this.FunctionTable.FirstOrDefault(f => f.Name.Equals(functionName));
		}
        /// <summary>
        /// 
        /// </summary>
        /// <param name="functionName"></param>
        /// <returns></returns>
        internal InheritedEntry FindFunctionInherited(string functionName)
        {
            return this.FindFunctionInherited(functionName, this);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="functionName"></param>
        /// <param name="program"></param>
        /// <returns></returns>
        internal InheritedEntry FindFunctionInherited(string functionName, Program program)
        {
            Function function = program.FindFunction(functionName);
            if (function == null)
                return null;

            if (!function.Flags.HasFlag(FunctionFlags.Inherited))
                return new InheritedEntry() { Function = function, Program = program };

            foreach (Inherit inh in program.Inherits)
            {
                InheritedEntry ie = this.FindFunctionInherited(functionName, inh.Program);
                if (ie != null)
                    return ie;
            }
            return null;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder builder = new StringBuilder();
            // first 18 bytes is program identification
            int progCounter = 18;

            int lineSize = ((string.Format("{0:X}", this.ProgramSize).Length + 1) >> 1) * 2;

            builder.AppendLine("Program: " + this.Filename);
            int ver = 2;
            builder.AppendFormat("ID: {0}-{1}.{2}.{3}.{4}\r\n", Encoding.UTF8.GetString(this.Binary, 0, 2), this.Load4(ver), this.Load4(ver+4), this.Load4(ver+8), this.Load4(ver+12));
            while (progCounter < this.ProgramSize)
            {
                OpCodes code = this.ReadOpCode(progCounter++);
                progCounter = this.DecodeInstruction(code, progCounter, builder, lineSize, false);
            }
            return builder.ToString();
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="code"></param>
        /// <param name="progCounter"></param>
        /// <param name="builder"></param>
        /// <param name="breakpoint"></param>
        /// <returns></returns>
        private int DecodeInstruction(OpCodes code, int progCounter, StringBuilder builder, int ls, bool breakpoint)
        {
            if (code != OpCodes.bp)
                builder.AppendFormat("{0}AO_{1:D"+ls+"}: {2} ", breakpoint?"B":" ", progCounter - 1, Enum.GetName(typeof(OpCodes), code));

            switch (code)
            {
                case OpCodes.acol:
                case OpCodes.col:
                    builder.AppendLine(this.Load1(progCounter++).ToString()); break;
                case OpCodes.ldc_i2:
                    builder.AppendLine(this.Load2(progCounter).ToString()); progCounter += 2; break;
                case OpCodes.sloc:
                case OpCodes.sglob:
                case OpCodes.sarg:
                case OpCodes.lstr:
                case OpCodes.lloc:
                case OpCodes.lglob:
                case OpCodes.larg:
                case OpCodes.ldc_i4:
                case OpCodes.catch_s:
                    builder.AppendLine(this.Load4(progCounter).ToString()); progCounter += 4; break;
                case OpCodes.jmp:
                case OpCodes.jmpf:
                case OpCodes.jmpt:
                    builder.AppendLine(string.Format("AO_{0:D" + ls + "}", this.Load4(progCounter))); progCounter += 4; break;
                case OpCodes.ldc_i8:
                    builder.AppendLine(this.Load8(progCounter).ToString()); progCounter += 8; break;
                case OpCodes.call:
                case OpCodes.call_virt:
                    builder.Append(this.Load2(progCounter)); progCounter += 2; builder.AppendLine(" " + this.Load1(progCounter++)); break;
                case OpCodes.call_i:
                    builder.Append(this.Load1(progCounter++)); builder.Append(" " + this.Load2(progCounter)); progCounter += 2; builder.AppendLine(" " + this.Load1(progCounter++)); break;
                case OpCodes.ldc_i4_0:
                case OpCodes.ldc_i4_1:
                case OpCodes.ldc_i1_0:
                case OpCodes.ldc_i1_1:
                case OpCodes.ldc_i2_0:
                case OpCodes.ldc_i2_1:
                case OpCodes.add:
                case OpCodes.sub:
                case OpCodes.gt:
                case OpCodes.gte:
                case OpCodes.lt:
                case OpCodes.lte:
                case OpCodes.ret:
                case OpCodes.ceq:
                case OpCodes.dup:
                case OpCodes.pop:
                case OpCodes.catch_e:
                case OpCodes.idx_e:
                case OpCodes.idx_ee:
                case OpCodes.idx_en:
                case OpCodes.idx_n:
                case OpCodes.idx_ne:
                case OpCodes.idx_nn:
                case OpCodes.nop:
                    builder.AppendLine(); break;
                case OpCodes.bp:
                    Breakpoint bp = Breakpoints[progCounter - 1];
                    progCounter = DecodeInstruction(bp.OrigOpCode, progCounter, builder, ls, true);
                    break;
            }

            return progCounter;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="pc"></param>
        /// <returns></returns>
        public OpCodes IsInstruction(int pc)
        {
            int _b = 0;
            int instr = 0;
            for (instr = 0; instr < pc; instr++)
            {
                if (this.Binary[instr] < (int)OpCodes.__S_INSTRUCTION)
                    _b = 0;
                else if (this.Binary[instr] < (int)OpCodes.__1B_INSTRUCTION)
                    _b = 1;
                else if (this.Binary[instr] < (int)OpCodes.__2B_INSTRUCTION)
                    _b = 2;
                else if (this.Binary[instr] < (int)OpCodes.__3B_INSTRUCTION)
                    _b = 3;
                else if (this.Binary[instr] < (int)OpCodes.__4B_INSTRUCTION)
                    _b = 4;
                else if (this.Binary[instr] < (int)OpCodes.__8B_INSTRUCTION)
                    _b = 8;

                instr += _b;
            }
            if (instr != pc)
                return OpCodes.bp;

            return (OpCodes)this.Binary[instr];
        }
		/// <summary>
		/// 
		/// </summary>
		/// <param name="program"></param>
		/// <param name="stream"></param>
		public void Decode()
		{
            if (_log.IsDebugEnabled)
                Log.Debug(this.GetType(), this.ToString());
		}
    }
}
